﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

namespace Xms.Abp.BackgroundJob;

public class BackgroundJobTrigger : IBackgroundJobTrigger
{
    protected const string DistributedLockName = "BackgroundJobTrigger";

    protected XmsBackgroundJobTriggerOptions JobTriggerOptions { get; }

    protected IAbpDistributedLock DistributedLock { get; }

    protected ICurrentTenant CurrentTenant { get; }

    protected IServiceScopeFactory ServiceScopeFactory { get; }

    protected AbpAsyncTimer Timer { get; }

    public IAbpLazyServiceProvider LazyServiceProvider { get; set; }

    protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>();

    protected ILogger Logger => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance);

    protected CancellationTokenSource StoppingTokenSource { get; private set;  }
    protected CancellationToken StoppingToken { get; private set; }
    private bool IsRunning { get; set; }

    public BackgroundJobTrigger(
        AbpAsyncTimer timer,
        IOptions<XmsBackgroundJobTriggerOptions> jobTriggerOptions,
        IServiceScopeFactory serviceScopeFactory,
        IAbpDistributedLock distributedLock,
        ICurrentTenant currentTenant)
    {
        ServiceScopeFactory = serviceScopeFactory;
        Timer = timer;
        Timer.Elapsed = TimerElapsed;
        DistributedLock = distributedLock;
        JobTriggerOptions = jobTriggerOptions.Value;
        Timer.Period = JobTriggerOptions.JobPollPeriod;
        CurrentTenant = currentTenant;
    }

    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        if (IsRunning) return;
        IsRunning = true;
        StoppingTokenSource ??= new CancellationTokenSource();
        StoppingToken = StoppingTokenSource.Token;
        Logger.LogDebug("Started background job schedule: " + ToString());
        await Task.CompletedTask;
        Timer.Start(cancellationToken);
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken = default)
    {
        if (!IsRunning) return;
        IsRunning = false;
        Timer.Stop(cancellationToken);
        Logger.LogDebug("Stopped background job schedule: " + ToString());
        StoppingTokenSource?.Cancel();
        StoppingTokenSource?.Dispose();
        StoppingTokenSource = null;
        await Task.CompletedTask;
    }

    public override string ToString()
    {
        return GetType().FullName;
    }

    private async Task TimerElapsed(AbpAsyncTimer timer)
    {
        using (var scope = ServiceScopeFactory.CreateScope())
        {
            var serviceProvider = scope.ServiceProvider;
            try
            {
                var uowm = serviceProvider.GetRequiredService<IUnitOfWorkManager>();
                using (var uow = uowm.Begin(true, false))
                {
                    await using var handler = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: StoppingToken);
                    if (handler != null)
                    {
                        var store = serviceProvider.GetRequiredService<IBackgroundJobStore>();

                        var jobExecuter = serviceProvider.GetRequiredService<IBackgroundJobExecuter>();
                        var clock = serviceProvider.GetRequiredService<IClock>();
                        var serializer = serviceProvider.GetRequiredService<IBackgroundJobSerializer>();

                        await ExecuteJobsAsync(serviceProvider, store, jobExecuter, clock, serializer);
                        //await uow.SaveChangesAsync();
                        await uow.CompleteAsync();
                    }
                    else
                    {
                        try
                        {
                            await Task.Delay(JobTriggerOptions.JobPollPeriod * 12, StoppingToken);
                        }
                        catch (TaskCanceledException) { }
                        await uow.RollbackAsync();
                    }
                }
            }
            catch (Exception ex)
            {
                await serviceProvider
                    .GetRequiredService<IExceptionNotifier>()
                    .NotifyAsync(new ExceptionNotificationContext(ex));

                Logger.LogException(ex);
            }
        }
    }

    protected virtual async Task ExecuteJobsAsync(IServiceProvider serviceProvider, IBackgroundJobStore store, IBackgroundJobExecuter jobExecuter, IClock clock, IBackgroundJobSerializer serializer)
    {
        var waitingJobs = await store.GetWaitingJobsAsync(JobTriggerOptions.MaxJobFetchCount);

        if (!waitingJobs.Any())
        {
            return;
        }

        //TODO: 将任务标注为执行中
        foreach (var jobInfo in waitingJobs)
        {
            jobInfo.TryCount++;
            jobInfo.LastTryTime = clock.Now;
            jobInfo.BackgroundJobStatus = BackgroundJobStatus.Executing;
            jobInfo.TryTimeoutTime = clock.Now.AddMilliseconds(jobInfo.Timeout ?? JobTriggerOptions.DefaultTimeout);
        }
        await TryUpdateAsync(store, waitingJobs);

        if (JobTriggerOptions.MaxDegreeOfParallelism == 1)
        {
            foreach (var jobInfo in waitingJobs)
            {
                await ExecuteJobAsync(jobInfo, serviceProvider, jobExecuter, clock, serializer);
            }
        }
        else
        {
            var groups = waitingJobs.GroupBy(r => r.Priority).ToDictionary(r => r.Key, r => r.ToList());
            var prioritys = groups.Keys.OrderByDescending(r => r);
            var parallelOptions = new ParallelOptions
            {
                MaxDegreeOfParallelism = JobTriggerOptions.MaxDegreeOfParallelism,
                CancellationToken = StoppingToken,
            };
            foreach (var priority in prioritys)
            {
                Parallel.ForEach(groups[priority], parallelOptions, async (jobInfo, state, index) =>
                {
                    using (var scope = ServiceScopeFactory.CreateScope())
                    {
                        await ExecuteJobAsync(jobInfo, scope.ServiceProvider, jobExecuter, clock, serializer);
                    }
                });
            }
        }
        await TryUpdateAsync(store, waitingJobs);

        await ExecuteJobsAsync(serviceProvider, store, jobExecuter, clock, serializer);
    }

    protected virtual async Task ExecuteJobAsync(BackgroundJobInfo jobInfo, IServiceProvider serviceProvider, IBackgroundJobExecuter jobExecuter, IClock clock, IBackgroundJobSerializer serializer)
    {
        try
        {
            var jobConfiguration = JobTriggerOptions.GetJob(jobInfo.Name);
            var jobArgs = serializer.Deserialize(jobInfo.JobArgs, jobConfiguration.ArgsType);
            var context = new JobExecutionContext(
                serviceProvider,
                jobConfiguration.JobType,
                jobArgs);

            try
            {
                using (CurrentTenant.Change(jobInfo.TenantId))
                {
                    await jobExecuter.ExecuteAsync(context, StoppingToken);
                }
                jobInfo.NextTryTime = DateTime.MaxValue;
                jobInfo.BackgroundJobStatus = BackgroundJobStatus.Executed;
                jobInfo.TryTimeoutTime = null;

                //await store.DeleteAsync(jobInfo.Id);
            }
            catch (BackgroundJobExecutionException ex)
            {
                var nextTryTime = CalculateNextTryTime(jobInfo, clock);

                if (nextTryTime.HasValue)
                {
                    jobInfo.NextTryTime = nextTryTime.Value;
                }
                else
                {
                    jobInfo.IsAbandoned = true;
                }
                jobInfo.BackgroundJobStatus = BackgroundJobStatus.Error;
                jobInfo.ErrorMessage = ex.InnerException.Message;
                jobInfo.TryTimeoutTime = null;
            }
        }
        catch (Exception ex)
        {
            Logger.LogException(ex);
            jobInfo.IsAbandoned = true;
            jobInfo.BackgroundJobStatus = BackgroundJobStatus.Error;
            jobInfo.ErrorMessage = ex.Message;
            jobInfo.TryTimeoutTime = null;
        }
    }

    protected virtual async Task TryUpdateAsync(IBackgroundJobStore store, List<BackgroundJobInfo> jobInfos)
    {
        try
        {
            await store.UpdateAsync(jobInfos, StoppingToken);
        }
        catch (Exception updateEx)
        {
            Logger.LogException(updateEx);
        }
    }

    protected virtual DateTime? CalculateNextTryTime(BackgroundJobInfo jobInfo, IClock clock)
    {
        if (jobInfo.MaxTryCount.HasValue && jobInfo.TryCount >= jobInfo.MaxTryCount)
        {
            return null;
        }
        double nextWaitDuration;

        if (jobInfo.RetryIntervals != null && jobInfo.RetryIntervals.Any())
        {
            if (jobInfo.RetryIntervals.Count >= jobInfo.TryCount)
            {
                nextWaitDuration = jobInfo.RetryIntervals[jobInfo.TryCount - 1];
            }
            else
            {
                nextWaitDuration = jobInfo.RetryIntervals.Last();
            }
        }
        else
        {
            nextWaitDuration = JobTriggerOptions.DefaultFirstWaitDuration *
                               (Math.Pow(JobTriggerOptions.DefaultWaitFactor, jobInfo.TryCount - 1));
        }

        var nextTryDate = jobInfo.LastTryTime?.AddSeconds(nextWaitDuration) ??
                          clock.Now.AddSeconds(nextWaitDuration);

        var timeout = jobInfo.Timeout ?? JobTriggerOptions.DefaultTimeout;

        if (nextTryDate.Subtract(jobInfo.CreationTime).TotalSeconds > timeout)
        {
            return null;
        }

        return nextTryDate;
    }
}
